﻿// <copyright file="ReceiverBroker.cs" company="Hallvard Korsgaard">
//  Microsoft Public License (Ms-PL). See license details here: http://enoceannet.codeplex.com/license
// </copyright>
// <author>Hallvard Korsgaard</author>

namespace EnOceanService
{
  using System;
  using System.Collections.Generic;
  using System.Diagnostics;
  using System.Linq;
  using System.ServiceModel;
  using System.ServiceModel.Discovery;
  using System.Text;
  using System.Threading;
  using System.Timers;
  using System.Xml;
  using EnOcean.Radio.Sensor;
  using ServiceContract;
  using StcEthernet;

  /// <summary>
  /// This class is responsible for dicovering and keeping track of receiver services
  /// and for sending telegrams to them.
  /// </summary>
  internal static class ReceiverBroker
  {
    /// <summary>
    /// Keeps track of last message from a sensor.
    /// </summary>
    private static Dictionary<long, SensorData> sensors = new Dictionary<long, SensorData>();

    /// <summary>
    /// Keeps track of the receiver services.
    /// </summary>
    private static Dictionary<string, SensorReceiver> receivers = new Dictionary<string, SensorReceiver>();

    /// <summary>
    /// Lock for accessing the receivers dictionary.
    /// </summary>
    private static ReaderWriterLock receiversLock = new ReaderWriterLock();

    /// <summary>
    /// The message used when pinging receivers.
    /// </summary>
    private static SensorData pingMessage;

    /// <summary>
    /// The service that receives announcement messages.
    /// </summary>
    private static ServiceHost announcementServiceHost;

    /// <summary>
    /// Timer for pinging receiver services.
    /// </summary>
    private static System.Timers.Timer pingTimer;

    /// <summary>
    /// Timer for running discovery.
    /// </summary>
    private static System.Timers.Timer discoveryTimer;

    /// <summary>
    /// The discovery client.
    /// </summary>
    private static DiscoveryClient dc;

    /// <summary>
    /// Criteria for finding receivers.
    /// </summary>
    private static FindCriteria find;

    /// <summary>
    /// Initializes static members of the ReceiverBroker class.
    /// </summary>
    static ReceiverBroker()
    {
      pingMessage = new SensorData()
      {
        SensorId = 0,
        Timestamp = DateTimeOffset.MinValue,
        EnOceanTelegram = new byte[14]
      };
    }

    /// <summary>
    /// Gets or sets the event log to use for logging.
    /// </summary>
    internal static EventLog EventLog
    {
      get;
      set;
    }

    /// <summary>
    /// Starts the announcement service and ping and discovery timers.
    /// </summary>
    internal static void Start()
    {
      AnnouncementService announce;

      pingTimer = new System.Timers.Timer(10000);
      pingTimer.Elapsed += new ElapsedEventHandler(PingTimer_Elapsed);
      pingTimer.AutoReset = true;
      pingTimer.Start();

      dc = new DiscoveryClient(new UdpDiscoveryEndpoint());
      dc.FindProgressChanged += new EventHandler<FindProgressChangedEventArgs>(FindProgressChanged);
      find = new FindCriteria(typeof(IEnOceanReceiver)) { Duration = TimeSpan.FromSeconds(5) };

      discoveryTimer = new System.Timers.Timer(15000);
      discoveryTimer.Elapsed += new ElapsedEventHandler(DiscoveryTimer_Elapsed);
      discoveryTimer.AutoReset = true;
      discoveryTimer.Start();

      // Initialize the announcement service.
      announce = new AnnouncementService();
      announce.OnlineAnnouncementReceived += new EventHandler<AnnouncementEventArgs>(OnlineAnnouncementReceived);
      announce.OfflineAnnouncementReceived += new EventHandler<AnnouncementEventArgs>(OfflineAnnouncementReceived);
      announcementServiceHost = new ServiceHost(announce);
      announcementServiceHost.AddServiceEndpoint(new UdpAnnouncementEndpoint());
      announcementServiceHost.Open();
    }

    /// <summary>
    /// Stops pinging, discovery and announcement service.
    /// </summary>
    internal static void Stop()
    {
      pingTimer.Stop();
      pingTimer = null;

      discoveryTimer.Stop();
      discoveryTimer = null;
      dc.Close();

      announcementServiceHost.Close();
      announcementServiceHost = null;
    }

    /// <summary>
    /// Adds a receiver service to the list of known receivers.
    /// </summary>
    /// <param name="address">The endpoint address.</param>
    internal static void AddReceiver(EndpointAddress address)
    {
      SensorReceiver sr;
      LockCookie lc;
      string key;

      key = address.ToString();

      try
      {
        receiversLock.AcquireReaderLock(-1);

        if (!receivers.ContainsKey(key))
        {
          sr = new SensorReceiver();
          sr.SensorReceiverProxy = new SensorReceiverProxy(address);
          sr.Subscriptions = sr.SensorReceiverProxy.Subscriptions();

          lc = receiversLock.UpgradeToWriterLock(-1);

          try
          {
            if (!receivers.ContainsKey(key))
            {
              receivers.Add(key, sr);
            }
          }
          finally
          {
            receiversLock.DowngradeFromWriterLock(ref lc);
          }
        }
      }
      finally
      {
        receiversLock.ReleaseReaderLock();
      }
    }

    /// <summary>
    /// Removes a receiver service from the list known receivers.
    /// </summary>
    /// <param name="key">The endpoint address.</param>
    internal static void RemoveReceiver(string key)
    {
      receiversLock.AcquireWriterLock(-1);

      try
      {
        receivers.Remove(key);
      }
      finally
      {
        receiversLock.ReleaseWriterLock();
      }
    }

    /// <summary>
    /// The event fires when a telegram is received from the transiever.
    /// The event handler code must be thread safe because several events can fire at the same time.
    /// </summary>
    /// <param name="sender">The object that sent the event.</param>
    /// <param name="e">The event data.</param>
    internal static void Transiever_TelegramReceived(object sender, TelegramReceivedEventArgs e)
    {
      SensorData data;
      SensorTelegram st;
      List<string> failed;

      try
      {
        if (e.Telegram is SensorTelegram)
        {
          st = (SensorTelegram)e.Telegram;

          data = new SensorData();
          data.Timestamp = DateTimeOffset.Now;
          data.SensorId = st.Id;
          data.EnOceanTelegram = st.GetBytes();

          lock (sensors)
          {
            if (!sensors.ContainsKey(data.SensorId))
            {
              sensors.Add(data.SensorId, data);
            }
            else
            {
              sensors[data.SensorId] = data;
            }
          }

          failed = new List<string>();

          receiversLock.AcquireReaderLock(-1);

          try
          {
            foreach (KeyValuePair<string, SensorReceiver> sr in receivers)
            {
              try
              {
                if (sr.Value.Subscriptions == null)
                {
                  sr.Value.SensorReceiverProxy.SensorUpdated(data);
                }
                else if (sr.Value.Subscriptions.Contains(data.SensorId))
                {
                  sr.Value.SensorReceiverProxy.SensorUpdated(data);
                }
              }
              catch (Exception ex)
              {
                failed.Add(sr.Key);
                EventLog.WriteEntry("Failed calling service\n" + ex.ToString(), EventLogEntryType.Error);
              }
            }
          }
          finally
          {
            receiversLock.ReleaseReaderLock();
          }

          if (failed.Count > 0)
          {
            receiversLock.AcquireWriterLock(-1);

            try
            {
              foreach (string key in failed)
              {
                receivers.Remove(key);
              }
            }
            finally
            {
              receiversLock.ReleaseWriterLock();
            }
          }
        }
        else
        {
          EventLog.WriteEntry("Telegram: " + e.Telegram.GetType().ToString() + " " + e.Telegram, EventLogEntryType.Information);
        }
      }
      catch (Exception ex)
      {
        EventLog.WriteEntry(ex.ToString(), EventLogEntryType.Error);
      }
    }

    /// <summary>
    /// Calls all receiver services. If a call fails the receiver service is removed from the list of known services.
    /// </summary>
    internal static void Ping()
    {
      List<string> failed;

      try
      {
        failed = new List<string>();

        receiversLock.AcquireReaderLock(-1);

        try
        {
          foreach (KeyValuePair<string, SensorReceiver> sr in receivers)
          {
            try
            {
              sr.Value.SensorReceiverProxy.SensorUpdated(pingMessage);
            }
            catch (Exception ex)
            {
              failed.Add(sr.Key);
              EventLog.WriteEntry("Failed pinging service\n" + ex.ToString(), EventLogEntryType.Error);
            }
          }
        }
        finally
        {
          receiversLock.ReleaseReaderLock();
        }

        if (failed.Count > 0)
        {
          receiversLock.AcquireWriterLock(-1);

          try
          {
            foreach (string key in failed)
            {
              receivers.Remove(key);
            }
          }
          finally
          {
            receiversLock.ReleaseWriterLock();
          }
        }
      }
      catch (Exception ex)
      {
        EventLog.WriteEntry(ex.ToString(), EventLogEntryType.Error);
      }
    }

    /// <summary>
    /// Event handler for the discovery timer. Start a new discovery.
    /// </summary>
    /// <param name="sender">The object that sent the event.</param>
    /// <param name="e">The event data.</param>
    private static void DiscoveryTimer_Elapsed(object sender, ElapsedEventArgs e)
    {
      try
      {
        dc.FindAsync(find);
      }
      catch (Exception ex)
      {
        EventLog.WriteEntry(ex.ToString(), EventLogEntryType.Error);
      }
    }

    /// <summary>
    /// Event handler for the ping timer.
    /// </summary>
    /// <param name="sender">The object that sent the event.</param>
    /// <param name="e">The event data.</param>
    private static void PingTimer_Elapsed(object sender, ElapsedEventArgs e)
    {
      try
      {
        ReceiverBroker.Ping();
      }
      catch (Exception ex)
      {
        EventLog.WriteEntry(ex.ToString(), EventLogEntryType.Error);
      }
    }

    /// <summary>
    /// Event handler for the discovery service.
    /// </summary>
    /// <param name="sender">The object that sent the event.</param>
    /// <param name="e">The event data.</param>
    private static void FindProgressChanged(object sender, FindProgressChangedEventArgs e)
    {
      try
      {
        ReceiverBroker.AddReceiver(e.EndpointDiscoveryMetadata.Address);
      }
      catch (Exception ex)
      {
        EventLog.WriteEntry(ex.ToString(), EventLogEntryType.Error);
      }
    }

    /// <summary>
    /// Event handler for the announcement service.
    /// </summary>
    /// <param name="sender">The object that sent the event.</param>
    /// <param name="e">The event data.</param>
    private static void OnlineAnnouncementReceived(object sender, AnnouncementEventArgs e)
    {
      try
      {
        foreach (XmlQualifiedName name in e.EndpointDiscoveryMetadata.ContractTypeNames)
        {
          if (!name.IsEmpty && name.Namespace == "http://enoceannet.codeplex.com/EnOceanReceiver/v1" && name.Name == "IEnOceanReceiver")
          {
            ReceiverBroker.AddReceiver(e.EndpointDiscoveryMetadata.Address);
            break;
          }
        }
      }
      catch (Exception ex)
      {
        EventLog.WriteEntry(ex.ToString(), EventLogEntryType.Error);
      }
    }

    /// <summary>
    /// Event handler for the announcement service.
    /// </summary>
    /// <param name="sender">The object that sent the event.</param>
    /// <param name="e">The event data.</param>
    private static void OfflineAnnouncementReceived(object sender, AnnouncementEventArgs e)
    {
      try
      {
        foreach (XmlQualifiedName name in e.EndpointDiscoveryMetadata.ContractTypeNames)
        {
          if (!name.IsEmpty && name.Namespace == "http://enoceannet.codeplex.com/EnOceanReceiver/v1" && name.Name == "IEnOceanReceiver")
          {
            ReceiverBroker.RemoveReceiver(e.EndpointDiscoveryMetadata.Address.ToString());
            break;
          }
        }
      }
      catch (Exception ex)
      {
        EventLog.WriteEntry(ex.ToString(), EventLogEntryType.Error);
      }
    }
  }
}
